/**
 * 
 */
package com.alphasystem.commerce.common.annotation.apt;

import static com.alphasystem.commerce.common.annotation.apt.UIAnnotationProcessor.OUT_FILE_OPTION;
import static com.alphasystem.commerce.common.annotation.apt.UIAnnotationProcessor.TEMPLATE_FILE_PATH;
import static java.lang.String.format;
import static java.lang.System.out;
import static javax.lang.model.SourceVersion.RELEASE_6;
import static org.apache.commons.lang.StringUtils.isEmpty;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.Temporal;

import com.alphasystem.commerce.common.annotation.ui.Attribute;
import com.alphasystem.commerce.common.annotation.ui.Component;
import com.alphasystem.commerce.common.annotation.ui.Property;
import com.alphasystem.commerce.common.util.AppUtil;
import com.alphasystem.commerce.ui.cmc.AttributeType;
import com.alphasystem.commerce.ui.cmc.ComponentType;
import com.alphasystem.commerce.ui.cmc.ObjectFactory;
import com.alphasystem.commerce.ui.cmc.PropertyType;

/**
 * @author sali
 * 
 */
@SupportedAnnotationTypes({ "*" })
@SupportedSourceVersion(RELEASE_6)
@SupportedOptions({ OUT_FILE_OPTION, TEMPLATE_FILE_PATH })
public class UIAnnotationProcessor extends AbstractProcessor {

	public static final String OUT_FILE_OPTION = "outFile";

	public static final String TEMPLATE_FILE_PATH = "templateFilePath";

	private File outFile;

	private URL templateFilePath;

	private Map<String, ComponentType> components = new TreeMap<String, ComponentType>();

	@SuppressWarnings("rawtypes")
	private void addPropertiesFromParent(ComponentType componentType,
			Class clazz) {
		try {
			BeanInfo info = Introspector.getBeanInfo(clazz);
			PropertyDescriptor[] props = info.getPropertyDescriptors();
			for (PropertyDescriptor propertyDescriptor : props) {
				Method getMethod = propertyDescriptor.getReadMethod();
				if (getMethod != null
						&& Modifier.isPublic(getMethod.getModifiers())) {
					Property annotation = null;
					annotation = (Property) getAnnotation(
							Property.class.getName(),
							getMethod.getAnnotations());
					if (annotation != null) {
						Map<String, Object> values = getValues(annotation);
						populatePropertyAttributes(getMethod.getName(),
								componentType.getTypeName(), values);
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void addPropertyTypeIfNotExists(ComponentType componentType,
			PropertyType propertyType) {
		List<PropertyType> propertyTypes = componentType.getProperty();
		boolean exists = false;
		for (PropertyType pt : propertyTypes) {
			if (pt.getFieldName().equals(propertyType.getFieldName())) {
				exists = true;
				break;
			}
		}
		if (!exists) {
			propertyTypes.add(propertyType);
		}
	}

	private AttributeType createAttributeType(String name, String value) {
		AttributeType attributeType = new AttributeType();
		attributeType.setName(name);
		attributeType.setValue(value);
		return attributeType;
	}

	private Annotation getAnnotation(String typeName, Annotation[] annotations) {
		for (int i = 0; i < annotations.length; i++) {
			if (annotations[i].annotationType().getName().equals(typeName))
				return annotations[i];
		}
		return null;
	}

	public File getOutFile() {
		return outFile;
	}

	private String getPropertyName(String name) {
		if (name.startsWith("get")) {
			name = String.valueOf(Character.toLowerCase(name.charAt(3)))
					+ name.substring(4);
		} else if (name.startsWith("is")) {
			name = String.valueOf(Character.toLowerCase(name.charAt(2)))
					+ name.substring(3);
		}
		return name;
	}

	public URL getTemplateFilePath() {
		return templateFilePath;
	}

	@SuppressWarnings("rawtypes")
	private Map<String, Object> getValues(Annotation annotation) {
		Map<String, Object> values = new TreeMap<String, Object>();
		// if the tag classes were on this project we could just cast to the
		// right type but they are needed on core
		Class annotationType = annotation.annotationType();
		Method[] methods = annotationType.getMethods();
		// iterate over class fields
		for (int i = 0; i < methods.length; ++i) {
			Method method = methods[i];
			if (method != null && method.getParameterTypes().length == 0) {
				try {
					Object value = method.invoke(annotation, new Object[0]);
					values.put(method.getName(), value);
				} catch (Exception e) {
					throw new RuntimeException(e.getMessage(), e);
				}
			}
		}
		return values;
	}

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);
		Map<String, String> options = processingEnv.getOptions();
		outFile = new File(options.get(OUT_FILE_OPTION));
		if (outFile == null) {
			throw new RuntimeException("Option \"outFile\" is missing");
		}
		File parentFile = outFile.getParentFile();
		if (!parentFile.exists()) {
			parentFile.mkdirs();
		}
		String option = options.get("templateFilePath");
		if (option == null) {
			throw new RuntimeException("Option \"templateFilePath\" is missing");
		}
		templateFilePath = AppUtil.getUrl(option);
		if (templateFilePath == null) {
			throw new RuntimeException("\"templateFile\" cannot be located");
		}
	}

	private void populateComponentAttributes(Element element) {
		Component component = element.getAnnotation(Component.class);
		if (component != null) {
			String name = element.toString();

			ObjectFactory objectFactory = new ObjectFactory();

			ComponentType componentType = objectFactory.createComponentType();
			componentType.setTypeName(name);

			String resourceBundle = component.resourceBundle();
			if (!isEmpty(resourceBundle)) {
				componentType.setResourceBundle(resourceBundle);
			}

			String displayName = component.displayName();
			if (isEmpty(displayName)) {
				displayName = name;
				int index = name.lastIndexOf('.');
				if (index > -1) {
					displayName = name.substring(index + 1);
				}
			}
			componentType.setDisplayName(displayName);
			components.put(name, componentType);
		}

	}

	@SuppressWarnings("rawtypes")
	private void populateDataType(String componentName, String methodName,
			PropertyType propertyType) {
		try {
			Class clazz = Class.forName(componentName);
			BeanInfo info = Introspector.getBeanInfo(clazz);
			PropertyDescriptor[] props = info.getPropertyDescriptors();
			for (PropertyDescriptor propertyDescriptor : props) {
				Method getMethod = propertyDescriptor.getReadMethod();
				if (getMethod != null
						&& Modifier.isPublic(getMethod.getModifiers())
						&& getMethod.getName().equals(methodName)) {
					Class<?> returnType = getMethod.getReturnType();
					if (returnType.isAssignableFrom(Set.class)
							|| returnType.isAssignableFrom(List.class)) {
						propertyType
								.setComponentType(((Class) ((ParameterizedType) getMethod
										.getGenericReturnType())
										.getActualTypeArguments()[0]).getName());
					} else if (returnType.isAssignableFrom(Map.class)) {
						propertyType
								.setComponentType(((Class) ((ParameterizedType) getMethod
										.getGenericReturnType())
										.getActualTypeArguments()[1]).getName());
					}

					if (returnType.isArray()) {
						propertyType.setDataType(Array.class.getName());
						propertyType.setComponentType(returnType
								.getComponentType().getName());

					} else if (returnType.isEnum()) {
						propertyType.getAttribute().add(
								createAttributeType("enumerated", "true"));
						propertyType.setDataType(returnType.getName());
					} else {
						String dataType = returnType.getName();
						Annotation[] annotations = getMethod.getAnnotations();
						if (dataType.equals(String.class.getName())) {
							Annotation lob = getAnnotation(Lob.class.getName(),
									annotations);
							Column column = (Column) getAnnotation(
									Column.class.getName(), annotations);
							int maxLength = -1;
							if (column != null) {
								maxLength = column.length();
							}
							if (lob == null) {
								propertyType.getAttribute().add(
										createAttributeType("maxLength",
												String.valueOf(maxLength)));
							}
						} else if (dataType.equals(Date.class.getName())) {
							Temporal temporal = (Temporal) getAnnotation(
									Temporal.class.getName(), annotations);
							System.out.println("|||||||||||| " + componentName
									+ "." + methodName + " : "
									+ temporal.value().name());
							propertyType.getAttribute().add(
									createAttributeType("dateType", temporal
											.value().name()));
						}
						propertyType.setDataType(dataType);
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	private void populatePropertyAttributes(String methodName,
			String componentName, Map<String, Object> values) {
		ObjectFactory objectFactory = new ObjectFactory();
		String name = getPropertyName(methodName);
		ComponentType componentType = components.get(componentName);
		if (componentType == null) {
			System.out.println("NULL " + componentName);
		} else {
			PropertyType propertyType = objectFactory.createPropertyType();
			populateDataType(componentName, methodName, propertyType);

			propertyType.setFieldName(name);
			String resourceBundle = (String) values.get("resourceBundle");
			if (isEmpty(resourceBundle)) {
				resourceBundle = componentType.getResourceBundle();
			}
			if (!isEmpty(resourceBundle)) {
				propertyType.setResourceBundle(resourceBundle);
			}
			String displayName = (String) values.get("displayName");
			if (isEmpty(displayName)) {
				displayName = name;
			}
			propertyType.setDisplayName(displayName);
			propertyType.setCategory((String) values.get("category"));
			propertyType.setRequired((Boolean) values.get("required"));
			List<AttributeType> attributeTypes = propertyType.getAttribute();
			Attribute[] attributes = (Attribute[]) values.get("attributes");
			for (Attribute attribute : attributes) {
				attributeTypes.add(createAttributeType(attribute.name(),
						attribute.value()));
			}

			addPropertyTypeIfNotExists(componentType, propertyType);
		}
	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations,
			RoundEnvironment roundEnv) {
		Set<? extends Element> elements = roundEnv
				.getElementsAnnotatedWith(Entity.class);
		processEntityClasses(elements);

		elements = roundEnv.getElementsAnnotatedWith(Component.class);
		if (elements.isEmpty()) {
			System.out.println("Nothing to process.");
			return true;
		}
		processComponentAnnotations(elements);

		elements = roundEnv.getElementsAnnotatedWith(Property.class);
		processPropertyAnnotations(elements);

		// we can't process the hierarchy on the first pass because apt does not
		// guarantees that the base classes will be processed before their
		// subclasses
		for (Entry<String, ComponentType> entry : components.entrySet()) {
			processHierarchy(entry.getValue());
		}
		saveAsXml();

		return true;
	}

	private void processComponentAnnotations(Set<? extends Element> elements) {
		for (Element element : elements) {
			populateComponentAttributes(element);
		}
	}

	private void processEntityClasses(Set<? extends Element> elements) {
		for (Element e : elements) {
			String name = e.toString();
			out.println(format("%s%s%s", "<class>", name, "</class>"));
		}
	}

	@SuppressWarnings("rawtypes")
	private void processHierarchy(ComponentType componentType) {
		try {
			Class clazz = Class.forName(componentType.getTypeName());
			clazz = clazz.getSuperclass();
			while (clazz != null
					&& !clazz.getName().equals(Object.class.getName())) {
				ComponentType parentComponent = components.get(clazz.getName());
				if (parentComponent == null) {
					addPropertiesFromParent(componentType, clazz);
				} else {
					List<PropertyType> propertyTypes = parentComponent
							.getProperty();
					for (PropertyType propertyType : propertyTypes) {
						addPropertyTypeIfNotExists(componentType, propertyType);
					}
				}
				clazz = clazz.getSuperclass();
			}
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}

	}

	private void processPropertyAnnotations(Set<? extends Element> elements) {
		for (Element element : elements) {
			Property property = element.getAnnotation(Property.class);
			Map<String, Object> values = getValues(property);
			String componentName = element.getEnclosingElement().toString();
			populatePropertyAttributes(element.getSimpleName().toString(),
					componentName, values);
		}
	}

	private void saveAsXml() {
		PersistenceHelper persistenceHelper = null;
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new InputStreamReader(
					templateFilePath.openStream()));
			persistenceHelper = new FreemarkerPersistenceHelperImpl(reader);
			persistenceHelper.saveAsXml(outFile, components);
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
				}
			}
		}
	}

}
